#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Nuitka打包脚本
根据项目需求配置不同目录的打包策略
"""

import os
import ctypes
import sys
import shutil
import subprocess
import logging
from pathlib import Path

# 设置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

def _get_short_path(p: str) -> str:
    try:
        buf = ctypes.create_unicode_buffer(260)
        r = ctypes.windll.kernel32.GetShortPathNameW(p, buf, len(buf))
        if r > 0:
            return buf.value
    except Exception:
        pass
    return p

def _numpy_data_args() -> list:
    args = []
    try:
        import numpy
        np_dir = Path(numpy.__file__).parent
        core_dir = np_dir / "core"
        alt_core_dir = np_dir / "_core"
        libs_dir = np_dir / "libs"
        def add_glob(src_dir: Path, pattern: str, dest_rel: str):
            try:
                from glob import glob
                files = glob(str(src_dir / pattern))
                if files:
                    args.append(f"--include-data-files={_get_short_path(str(src_dir))}\\{pattern}={dest_rel}")
            except Exception:
                pass
        if core_dir.exists():
            add_glob(core_dir, "*.pyd", "numpy/core/")
            add_glob(core_dir, "*.dll", "numpy/core/")
        if alt_core_dir.exists():
            add_glob(alt_core_dir, "*.pyd", "numpy/_core/")
            add_glob(alt_core_dir, "*.dll", "numpy/_core/")
        if libs_dir.exists():
            add_glob(libs_dir, "*.dll", "numpy/libs/")
        try:
            env_root = np_dir.parents[2]
            bin_dir = env_root / "Library" / "bin"
            if bin_dir.exists():
                patterns = ["mkl*.dll", "libiomp*.dll", "openblas*.dll", "lapack*.dll", "blas*.dll"]
                from glob import glob
                for pat in patterns:
                    if glob(str(bin_dir / pat)):
                        args.append(f"--include-data-files={_get_short_path(str(bin_dir))}\\{pat}=./")
        except Exception:
            pass
    except Exception:
        pass
    return args


def _pyqtgraph_package_args() -> list:
    args = []
    try:
        args.append("--include-package=pyqtgraph")
        args.append("--include-package=pyqtgraph.opengl")
    except Exception:
        args.append("--include-package=pyqtgraph")
        args.append("--include-package=pyqtgraph.opengl")
    return args

def _opengl_nofollow_args() -> list:
    args = []
    try:
        args.extend([
            "--nofollow-import-to=OpenGL",
            "--noinclude-custom-mode=OpenGL:nofollow",
            "--nofollow-import-to=OpenGL.platform",
            "--noinclude-custom-mode=OpenGL.platform:nofollow",
            "--nofollow-import-to=OpenGL.platform.win32",
            "--noinclude-custom-mode=OpenGL.platform.win32:nofollow",
            "--nofollow-import-to=OpenGL.GL",
            "--noinclude-custom-mode=OpenGL.GL:nofollow",
        ])
    except Exception:
        pass
    return args

def _numpy_nofollow_args() -> list:
    args = []
    try:
        args.extend([
            "--nofollow-import-to=numpy",
            "--noinclude-custom-mode=numpy:nofollow",
            "--nofollow-import-to=numpy.compat",
            "--noinclude-custom-mode=numpy.compat:nofollow",
        ])
    except Exception:
        pass
    return args

def _detect_compiler_flag() -> str:
    try:
        import shutil
        # 优先使用已安装的 MSVC，稳定性更好
        if shutil.which("cl.exe"):
            logger.info("检测到MSVC，使用 --msvc=14.3")
            return "--msvc=14.3"
        if shutil.which("clang-cl"):
            logger.info("检测到clang-cl，使用 --clang (避免MSVC C23 embed问题)")
            return "--clang"
        if shutil.which("gcc"):
            logger.info("检测到gcc (MinGW64)，使用 --mingw64")
            return "--mingw64"
    except Exception as e:
        logger.warning(f"检测编译器时出错: {e}")
    
    logger.info("未找到合适编译器，回退到 --msvc=14.2")
    return "--msvc=14.2"

# -------------------------
# OpenGL运行时文件收集（Qt/Mesa 依赖），提升在不同机器上的渲染兼容性
def _qt_opengl_runtime_files() -> list:
    files = []
    try:
        import PySide2
        base = Path(PySide2.__file__).resolve().parent / "Qt" / "bin"
        candidates = [
            base / "opengl32sw.dll",
            base / "libEGL.dll",
            base / "libGLESv2.dll",
            base / "d3dcompiler_47.dll",
            base / "libeay32.dll",
            base / "ssleay32.dll",
        ]
        libbin = Path(sys.prefix) / "Library" / "bin"
        candidates += [
            libbin / "opengl32sw.dll",
            libbin / "libEGL.dll",
            libbin / "libGLESv2.dll",
            libbin / "d3dcompiler_47.dll",
            libbin / "libeay32.dll",
            libbin / "ssleay32.dll",
            # Mesa软件渲染库
            libbin / "osmesa.dll",
            libbin / "glapi.dll",
            libbin / "swrAVX.dll",
            libbin / "swrAVX2.dll",
            libbin / "swrSKX.dll",
        ]
        conda_prefix = os.environ.get("CONDA_PREFIX", "")
        if conda_prefix:
            conda_libbin = Path(conda_prefix) / "Library" / "bin"
            candidates += [
                conda_libbin / "opengl32sw.dll",
                conda_libbin / "osmesa.dll",
                conda_libbin / "glapi.dll",
            ]
        seen = set()
        for p in candidates:
            try:
                rp = p.resolve()
            except Exception:
                rp = p
            if rp.exists():
                sp = str(rp)
                if sp not in seen:
                    files.append(sp)
                    seen.add(sp)
    except Exception as exc:
        logger.warning("Failed to collect OpenGL runtime files: %s", exc)
    logger.info("Found %d OpenGL runtime files", len(files))
    return files

def build_with_nuitka_fixed():
    logger.info("开始Nuitka打包...")
    build_dir_short = _get_short_path(str(BUILD_DIR))
    project_root_short = _get_short_path(str(PROJECT_ROOT))
    scons_temp_dir = _get_short_path(str(Path("C:/_mt_scons").absolute()))
    try:
        Path(scons_temp_dir).mkdir(parents=True, exist_ok=True)
    except Exception:
        pass
    compiler_flag = _detect_compiler_flag()
    try:
        cpu_total = os.cpu_count() or 4
    except Exception:
        cpu_total = 4
    jobs_count = max(1, cpu_total - 1)
    nuitka_args = [
        sys.executable, "-m", "nuitka",
        "--standalone",
        "--windows-console-mode=disable",
        compiler_flag,
        f"--jobs={jobs_count}",
        "--show-memory",
        "--show-progress",
        "--windows-uac-admin",
        # "--disable-ccache",
        "--lto=no",
        # 移除低内存模式以规避 Nuitka 上下文崩溃问题

        "--windows-product-name=MooeTool",
        "--windows-company-name=MooeTech",
        "--windows-file-version=2.3.1.0",
        "--windows-product-version=2.3.1.0",

        "--include-qt-plugins=platforms,imageformats,styles,iconengines,printsupport",
        "--enable-plugin=pyside2",
        "--enable-plugin=numpy",
        

        "--include-package=rosbags",
        "--include-package=websockets",


        "--follow-import-to=apis,gglobal,mooetoolapp",

        "--include-data-dir=resource=resource",
        "--include-data-dir=config=config",
        "--windows-icon-from-ico=resource/qrc/bitbug_favicon.ico",
        f"--output-dir={build_dir_short}",
        "--output-filename=MooeTool.exe",
        "main.py",
    ]

    # 添加 numpy 依赖参数
    nuitka_args.extend(_numpy_data_args())
    # 添加 pyqtgraph.opengl 与 OpenGL 相关包
    nuitka_args.extend(_pyqtgraph_package_args())
    # 将 PyOpenGL 标记为 nofollow，避免编译其海量 raw 子模块，按字节码打包
    nuitka_args.extend(_opengl_nofollow_args())
    # 将 NumPy 标记为 nofollow，避免编译大量纯Python模块，改为字节码打包
    nuitka_args.extend(_numpy_nofollow_args())
    
    # 添加 OpenGL 运行时文件（Qt 软件渲染与依赖，增强兼容性）
    for f in _qt_opengl_runtime_files():
        nuitka_args.append(f"--include-data-files={_get_short_path(f)}=./")
    try:
        logger.info(f"执行命令: {' '.join(nuitka_args)}")
        env = os.environ.copy()
        env["CLCACHE_DISABLE"] = "1"
        env["NUITKA_DISABLE_CCACHE"] = "1"
        env["NUITKA_SCONS_TEMP"] = scons_temp_dir
        env["TMP"] = scons_temp_dir
        env["TEMP"] = scons_temp_dir
        if compiler_flag == "--mingw64":
            mingw_bin = Path(sys.prefix) / "Library" / "mingw-w64" / "bin"
            if mingw_bin.exists():
                env["PATH"] = str(mingw_bin) + os.pathsep + env.get("PATH", "")
            env["CC"] = "gcc"
            env["CXX"] = "g++"
            env["LD"] = "g++"
            for k in ("VSINSTALLDIR","VCINSTALLDIR","VisualStudioVersion","CL","LINK","LIB","INCLUDE"):
                env.pop(k, None)
        subprocess.run(nuitka_args, cwd=project_root_short, check=True, env=env)
        logger.info("Nuitka打包成功!")
    except subprocess.CalledProcessError as e:
        logger.error(f"Nuitka打包失败: {e}")
        return False
    except FileNotFoundError:
        logger.error("错误: 未找到nuitka，请先安装: pip install nuitka")
        return False
    return True

# 项目根目录
PROJECT_ROOT = Path(__file__).parent.absolute()
# 使用短路径输出目录以规避Windows长路径/链接参数限制
DEFAULT_BUILD_DIR = PROJECT_ROOT / "build"
FALLBACK_BUILD_DIR = Path("C:/_mt_build").absolute()
BUILD_DIR = FALLBACK_BUILD_DIR

def clean_build_dir():
    """清理构建目录"""
    if BUILD_DIR.exists():
        logger.info(f"清理构建目录: {BUILD_DIR}")
        try:
            shutil.rmtree(BUILD_DIR)
        except Exception:
            pass
    BUILD_DIR.mkdir(exist_ok=True)

def copy_directory(src, dst, exclude_content=False):
    """复制目录
    
    Args:
        src: 源目录
        dst: 目标目录
        exclude_content: 如果为True，只创建目录结构，不复制内容
    """
    src_path = Path(src)
    dst_path = Path(dst)
    
    if not src_path.exists():
        logger.warning(f"警告: 源目录不存在 {src_path}")
        return
    
    if exclude_content:
        # 只创建目录，不复制内容
        dst_path.mkdir(parents=True, exist_ok=True)
        logger.info(f"创建空目录: {dst_path}")
    else:
        # 复制整个目录
        if dst_path.exists():
            shutil.rmtree(dst_path)
        shutil.copytree(src_path, dst_path)
        logger.info(f"复制目录: {src_path} -> {dst_path}")

def copy_msg_files():
    """复制ros1/msg目录和vehicle目录到apis模块中"""
    # 复制msg目录
    src_msg = PROJECT_ROOT / "apis" / "ros1" / "msg"
    dst_msg = BUILD_DIR /"main.dist"/ "apis" / "ros1" / "msg"
    
    if src_msg.exists():
        dst_msg.parent.mkdir(parents=True, exist_ok=True)
        copy_directory(src_msg, dst_msg)
        logger.info(f"复制msg目录: {src_msg} -> {dst_msg}")
    else:
        logger.warning(f"警告: msg目录不存在 {src_msg}")
    
    # 复制vehicle目录
    src_vehicle = PROJECT_ROOT / "apis" / "ros1" / "vehicle"
    dst_vehicle = BUILD_DIR /"main.dist"/ "apis" / "ros1" / "vehicle"
    
    if src_vehicle.exists():
        dst_vehicle.parent.mkdir(parents=True, exist_ok=True)
        copy_directory(src_vehicle, dst_vehicle)
        logger.info(f"复制vehicle目录: {src_vehicle} -> {dst_vehicle}")
    else:
        logger.warning(f"警告: vehicle目录不存在 {src_vehicle}")

def post_build_setup():
    db_dst = BUILD_DIR / "main.dist" / "db"
    db_dst.mkdir(exist_ok=True)
    logger.info(f"创建空db目录: {db_dst}")
    log_dst = BUILD_DIR / "main.dist" / "log"
    log_dst.mkdir(exist_ok=True)
    logger.info(f"创建空log目录: {log_dst}")
    pcd_dst = BUILD_DIR / "main.dist" / "resource" / "pcd_download"
    pcd_dst.mkdir(parents=True, exist_ok=True)
    logger.info(f"创建空pcd_download目录: {pcd_dst}")
    copy_msg_files()
    logger.info("打包后设置完成!")

def main():
    """主函数"""
    logger.info("=== MooeTool Nuitka 打包脚本 ===")
    logger.info(f"项目根目录: {PROJECT_ROOT}")
    logger.info(f"构建目录: {BUILD_DIR}")
    try:
        logger.info(f"Python解释器: {sys.executable}")
        import importlib.metadata as _md
        try:
            _np_ver = _md.version("numpy")
            logger.info(f"NumPy版本: {_np_ver}")
        except Exception:
            logger.info("NumPy未安装或不可用")
    except Exception:
        pass
    try:
        if (sys.version_info.major, sys.version_info.minor) != (3, 10):
            logger.warning("当前Python版本非3.10，建议使用3.10以匹配依赖")
    except Exception:
        pass
    
    try:
        # 1. 清理构建目录
        clean_build_dir()
        
        # 2. 使用Nuitka打包
        if not build_with_nuitka_fixed():
            logger.error("打包失败，退出")
            return False
        try:
            post_build_setup()
        except Exception:
            pass
        
        logger.info("\n=== 打包完成 ===")
        logger.info(f"输出目录: {BUILD_DIR}")
        logger.info("\n打包策略总结:")
        logger.info("1. ✓ apis - 作为整体模块打包，msg目录已复制")
        logger.info("2. ✓ config - 作为数据文件打包")
        logger.info("3. ✓ db - 只创建空目录")
        logger.info("4. ✓ docs - 不处理")
        logger.info("5. ✓ gglobal - 作为整体模块打包")
        logger.info("6. ✓ log - 只创建空目录")
        logger.info("7. ✓ mooetoolapp - 作为整体模块打包")
        logger.info("8. ✓ resource - 作为数据文件打包")
        logger.info("9. ✓ testcase - 不处理")
        logger.info("10. ✓ main.py - 主程序入口")
        logger.info("11. ✓ resource/script - 已包含在输出目录")
        logger.info("12. ✓ resource/pcd_download - 已创建空目录")
        logger.info("13. ✓ 图形视图 - 使用 pyqtgraph.opengl 渲染")
        logger.info("14. ✓ PyOpenGL 与 pyqtgraph.opengl 已打包，Qt OpenGL 运行库已包含")
        
        return True
        
    except Exception as e:
        logger.error(f"打包过程中发生错误: {e}")
        import traceback
        traceback.print_exc()
        return False

if __name__ == "__main__":
    success = main()
    sys.exit(0 if success else 1)
